package com.cq.hd.member.biz;

import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.cq.hd.common.constant.RedisKeyConstant;
import com.cq.hd.common.entity.Sms;
import com.cq.hd.common.enums.ExceptionEnum;
import com.cq.hd.common.enums.biz.SmsTypeEnum;
import com.cq.hd.common.response.Result;
import com.cq.hd.common.response.ResultGenerator;
import com.cq.hd.common.response.Throw;
import com.cq.hd.common.utils.IpUtil;
import com.cq.hd.common.utils.LocalDateUtil;
import com.cq.hd.common.utils.RedisUtil;
import com.cq.hd.common.utils.StringUtils;
import com.cq.hd.member.api.dto.SmsDto;
import com.cq.hd.member.api.dto.SmsVerifyDto;
import com.cq.hd.member.config.AliyunSmsConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

@Component
@Slf4j
public class SmsBiz {

    /**
     * 短信发送ip限流次数
     */
    private final static int SEND_MAX_IP_LIMIT_COUNT = 10;

    /**
     * 短信发送次数
     */
    private final static int SEND_MAX_COUNT = 100;

    /**
     * 验证码存储redis过期时间
     */
    private static final long SMS_VERIFICATION_CODE_TIME_OUT_VALUE = 30;

    /**
     * 验证码发送次数最大值
     */
    private static final int SMS_VERIFICATION_CODE_MAX_RETRY_COUNT = 5;

    @Autowired
    private AliyunSmsConfig aliyunSmsConfig;

    @Value("${aliyun.sms.enterSuccessCode}")
    private String enterSuccessCode;

    @Value("${aliyun.sms.enterFailCode}")
    private String enterFailCode;

    @Value("${aliyun.sms.commonCode}")
    private String commonCode;

    @Autowired
    private RedisUtil redisUtil;

    public Result<Object> sendSms(SmsDto smsDto) {
        if (StringUtils.isNullOrEmpty(smsDto.getMobile())) {
            Throw.isBusinessException("手机号码不能为空");
        }

        Integer type = smsDto.getType();
        if (type == null || SmsTypeEnum.typeOf(type) == null) {
            Throw.isBusinessException("验证码类型错误");
        }

        //根据ip限流
        if (!judgeSendSmsLimit()) {
            Throw.isBusinessException("请求过于频繁，请稍后重试");
        }

        return callAliYunSms(smsDto);
    }

    private Result<Object> callAliYunSms(SmsDto smsDto) {
        String code = StringUtils.getRandomNumString(6);

        try {
            Map<String, Object> map = new HashMap<>();
            map.put("code", code);
            aliyunSmsConfig.sendSms(smsDto.getMobile(), commonCode, JSON.toJSONString(map), null);
        } catch (Exception e) {
            log.error("发送短信验证码接口失败，错误日志：", e);
            return ResultGenerator.error("发送短信验证码失败");
        }

        //将验证码存储到Redis
        generateCode(smsDto, code);

        return ResultGenerator.success("发送成功");
    }

    private void generateCode(SmsDto smsDto, String code) {
        //验证码防刷 判断该手机号码当天发送验证码是否超过配置的阈值
        long count = redisUtil.incr(String.format(RedisKeyConstant.SMS_SEND_COUNT_PREFIX_KEY, smsDto.getMobile()));

        if (count > SEND_MAX_COUNT) {
            Throw.isBusinessException("验证码发送过于频繁");
        } else {
            // 验证码次数凌晨清除
            Duration duration = Duration.between(LocalDateTime.now(), LocalDate.now().plusDays(1).atTime(0, 0, 0));
            redisUtil.expire(String.format(RedisKeyConstant.SMS_SEND_COUNT_PREFIX_KEY, smsDto.getMobile()), duration.toMinutes(), TimeUnit.MINUTES);
        }

        LocalDateTime nowLocalDateTime = LocalDateUtil.getNowLocalDateTime();
        LocalDateTime expiredLocalDateTime = nowLocalDateTime.plusMinutes(SMS_VERIFICATION_CODE_TIME_OUT_VALUE);
        Sms sms = new Sms();
        sms.setCode(code);
        sms.setMobile(smsDto.getMobile());
        sms.setCreateTime(nowLocalDateTime);
        sms.setExpiredTime(expiredLocalDateTime);
        sms.setRetry(0);
        redisUtil.set(String.format(RedisKeyConstant.SMS_PREFIX_KEY, smsDto.getMobile(), SmsTypeEnum.typeOf(smsDto.getType()).getName()), JSON.toJSONString(sms),
                SMS_VERIFICATION_CODE_TIME_OUT_VALUE * 60 * 1000, TimeUnit.MILLISECONDS);
    }

    /**
     * 根据ip限流，一个ip每分钟发10个，超过需要等待1分钟
     */
    private boolean judgeSendSmsLimit() {
        try {
            HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();

            String ipNumToRedis = "1";
            String ipAddr = IpUtil.getIpAddr(request);
            log.info("ipAddr:" + ipAddr);
            String ipNumStr = redisUtil.get(ipAddr);
            if (!StringUtils.isNullOrEmpty(ipNumStr)) {
                int ipNum = Integer.parseInt(ipNumStr);
                if (ipNum >= SEND_MAX_IP_LIMIT_COUNT) {
                    //限流
                    log.error("请求过于频繁，ip:【" + ipAddr + "】被限流");
                    return false;
                }

                ipNum += 1;
                ipNumToRedis = String.valueOf(ipNum);
            }
            redisUtil.set(ipAddr, ipNumToRedis, 1, TimeUnit.MINUTES);

            return true;
        } catch (Exception e) {
            log.error("ip限流判断失败，失败原因：" + e);
            return false;
        }
    }

    public Result<Object> verify(SmsVerifyDto smsVerifyDto) {
        String smsJson = redisUtil.get(String.format(RedisKeyConstant.SMS_PREFIX_KEY, smsVerifyDto.getMobile(), SmsTypeEnum.typeOf(smsVerifyDto.getType()).getName()));

        if (StringUtils.isNullOrEmpty(smsJson)) {
            return ResultGenerator.fail(ExceptionEnum.SMS_CODE_NOT_FOUND, null);
        }

        return handleVerify(smsJson, smsVerifyDto);
    }

    private Result<Object> handleVerify(String smsJson, SmsVerifyDto smsVerifyDto) {
        ExceptionEnum error;
        Sms sms = JSONObject.parseObject(smsJson, Sms.class);

        if (LocalDateUtil.getNowLocalDateTime().isAfter(sms.getExpiredTime())) {
            //如果验证码过期
            error = ExceptionEnum.SMS_CODE_TIME_OUT;
        } else if (sms.getRetry() > SMS_VERIFICATION_CODE_MAX_RETRY_COUNT) {
            //如果验证码尝试次数超过指定次数
            error = ExceptionEnum.SMS_CODE_RETRY;
        } else if (ObjectUtil.notEqual(smsVerifyDto.getCode(), sms.getCode())) {
            //如果验证码输入错误
            error = ExceptionEnum.SMS_CODE_FAILED;
        } else {
            redisUtil.del(String.format(RedisKeyConstant.SMS_PREFIX_KEY, smsVerifyDto.getMobile(), SmsTypeEnum.typeOf(smsVerifyDto.getType()).getName()));
            return ResultGenerator.success("验证成功");
        }

        sms.setRetry(sms.getRetry() + 1);
        smsJson = JSON.toJSONString(sms);

        redisUtil.set(String.format(RedisKeyConstant.SMS_PREFIX_KEY, smsVerifyDto.getMobile(), SmsTypeEnum.typeOf(smsVerifyDto.getType()).getName()),
                smsJson, LocalDateUtil.toLocalDateTimeToLong(sms.getExpiredTime()) - System.currentTimeMillis(), TimeUnit.MILLISECONDS);

        return ResultGenerator.fail(error, null);
    }

    public Boolean sendEnterSuccessSms(String name, String mobile) {
        try {
            Map<String, Object> map = new HashMap<>();
            map.put("name", name);
            aliyunSmsConfig.sendSms(mobile, enterSuccessCode, JSON.toJSONString(map), null);
            return true;
        } catch (Exception e) {
            log.error("发送入驻成功短信验证码接口失败，错误日志：", e);
        }
        return false;
    }

    public Boolean sendEnterFailSms(String name, String mobile) {
        try {
            Map<String, Object> map = new HashMap<>();
            map.put("name", name);
            aliyunSmsConfig.sendSms(mobile, enterFailCode, JSON.toJSONString(map), null);
            return true;
        } catch (Exception e) {
            log.error("发送入驻失败短信验证码接口失败，错误日志：", e);
        }
        return false;
    }
}
